<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>18 Observable Operators - switchMap, mergeMap, concatMap · ShaofeiZi Blog · 做个日常记录</title>
    <meta name="description" content="訾绍飞的博客。万物皆有裂缝处，那是光射进来的地方。">
    <link rel="shortcut icon" href="/BLOG/favicon.ico">
  <link rel="manifest" href="/BLOG/manifest.json">
  <meta name="theme-color" content="#3F51B5">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <link rel="apple-touch-icon" href="/BLOG/icons/192.png">
  <link rel="mask-icon" href="/BLOG/icons/safari-pinned-tab.svg" color="#3eaf7c">
  <meta name="msapplication-TileImage" content="/icons/192.png">
  <meta name="msapplication-TileColor" content="#3F51B5">
    
    <link rel="preload" href="/BLOG/assets/css/42.styles.90045bd1.css" as="style"><link rel="preload" href="/BLOG/assets/js/app.1a725be8.js" as="script"><link rel="preload" href="/BLOG/assets/js/16.cd99d056.js" as="script"><link rel="prefetch" href="/BLOG/assets/js/7.88ba0bb7.js"><link rel="prefetch" href="/BLOG/assets/js/0.d3e592bd.js"><link rel="prefetch" href="/BLOG/assets/js/1.39b9c99c.js"><link rel="prefetch" href="/BLOG/assets/js/2.68dc10c9.js"><link rel="prefetch" href="/BLOG/assets/js/3.dfebdd5e.js"><link rel="prefetch" href="/BLOG/assets/js/4.ea97a821.js"><link rel="prefetch" href="/BLOG/assets/js/5.d8c2ecbf.js"><link rel="prefetch" href="/BLOG/assets/js/6.e51cd79c.js"><link rel="prefetch" href="/BLOG/assets/js/8.d9eebc06.js"><link rel="prefetch" href="/BLOG/assets/js/9.1a541d13.js"><link rel="prefetch" href="/BLOG/assets/js/10.4ec9ca67.js"><link rel="prefetch" href="/BLOG/assets/js/11.02558377.js"><link rel="prefetch" href="/BLOG/assets/js/12.d0e2086f.js"><link rel="prefetch" href="/BLOG/assets/js/13.5af02ddd.js"><link rel="prefetch" href="/BLOG/assets/js/14.5d9fcbf2.js"><link rel="prefetch" href="/BLOG/assets/js/15.ca0178b2.js"><link rel="prefetch" href="/BLOG/assets/js/17.56f11c1d.js"><link rel="prefetch" href="/BLOG/assets/js/18.21837cc7.js"><link rel="prefetch" href="/BLOG/assets/js/19.73335fea.js"><link rel="prefetch" href="/BLOG/assets/js/20.1632ab79.js"><link rel="prefetch" href="/BLOG/assets/js/21.43175244.js"><link rel="prefetch" href="/BLOG/assets/js/22.5b7c0cca.js"><link rel="prefetch" href="/BLOG/assets/js/23.e624ba97.js"><link rel="prefetch" href="/BLOG/assets/js/24.ac5f7b41.js"><link rel="prefetch" href="/BLOG/assets/js/25.6934a11d.js"><link rel="prefetch" href="/BLOG/assets/js/26.407b2583.js"><link rel="prefetch" href="/BLOG/assets/js/27.7449d673.js"><link rel="prefetch" href="/BLOG/assets/js/28.52e25437.js"><link rel="prefetch" href="/BLOG/assets/js/29.fba21f3a.js"><link rel="prefetch" href="/BLOG/assets/js/30.2cd6d3e2.js"><link rel="prefetch" href="/BLOG/assets/js/31.0b0a749f.js"><link rel="prefetch" href="/BLOG/assets/js/32.92134487.js"><link rel="prefetch" href="/BLOG/assets/js/33.ad2b89cc.js"><link rel="prefetch" href="/BLOG/assets/js/34.9b22334e.js"><link rel="prefetch" href="/BLOG/assets/js/35.825f3d75.js"><link rel="prefetch" href="/BLOG/assets/js/36.cc3da84c.js"><link rel="prefetch" href="/BLOG/assets/js/37.8f339f62.js"><link rel="prefetch" href="/BLOG/assets/js/38.5674618f.js"><link rel="prefetch" href="/BLOG/assets/js/39.180f0d85.js"><link rel="prefetch" href="/BLOG/assets/js/40.275f26e3.js"><link rel="prefetch" href="/BLOG/assets/js/41.ce0f5927.js">
    <link rel="stylesheet" href="/BLOG/assets/css/42.styles.90045bd1.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div data-app="true" id="app" class="application theme--light"><div class="application--wrap"><div class="v-progress-linear blog-progress" style="height:3px;display:none;"><div class="v-progress-linear__background accent" style="height:3px;opacity:0.4;width:100%;"></div><div class="v-progress-linear__bar"><!----><div class="v-progress-linear__bar__determinate accent" style="width:0%;"></div></div></div><aside class="v-navigation-drawer v-navigation-drawer--close v-navigation-drawer--fixed v-navigation-drawer--is-mobile" style="height:100%;margin-top:0px;max-height:calc(100% - 0px);transform:translateX(-240px);width:240px;"><div><div class="aside-brand-wrap"><div class="aside-brand"><a href="/BLOG/" class="aside-avatar elevation-2 router-link-active"><img src="/BLOG/face.png" alt="avatar"></a><hgroup class="mt-3 variant-hide"><div class="subheading white--text">訾绍飞</div><a href="mailto:zishaofei221@gmail.com" title="zishaofei221@gmail.com" class="aside-mail primary--text text--lighten-5">zishaofei221@gmail.com</a></hgroup></div></div><hr class="v-divider theme--dark"><div class="v-list nav-list"><div class="secondary--text"><a href="/BLOG/" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-home"></i></div></div><div class="v-list__tile__content">首页</div></a></div><div class="secondary--text"><a href="/BLOG/tags" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-tag"></i></div></div><div class="v-list__tile__content">标签</div></a></div><div class="secondary--text"><a href="https://github.com/ShaofeiZi" target="_blank" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fab fa-github"></i></div></div><div class="v-list__tile__content">Github</div></a></div><div class="secondary--text"><a href="/BLOG/about" class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-user-secret"></i></div></div><div class="v-list__tile__content">About</div></a></div></div></div><div class="v-navigation-drawer__border"></div></aside><nav class="blog-toolbar v-toolbar v-toolbar--fixed theme--dark primary" style="margin-top:0px;padding-right:0px;padding-left:0px;transform:translateY(0px);"><div class="v-toolbar__content" style="height:56px;"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-bars"></i></div></button><div class="v-toolbar__title">18 Observable Operators - switchMap, mergeMap, concatMap</div><div class="spacer"></div><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><div class="v-menu" style="display:none;"><div class="v-menu__activator"><button type="button" class="v-btn v-btn--icon"><div class="v-btn__content"><i class="fa fa-share-alt"></i></div></button></div><div class="v-menu__content" style="max-height:auto;min-width:0px;max-width:auto;top:12px;left:0px;transform-origin:top right;z-index:0;display:none;"><div class="v-list"><div class="secondary--text"><a class="v-list__tile v-list__tile--link"><div class="v-list__tile__avatar"><div class="v-avatar" style="height:40px;width:40px;"><i class="fa fa-lg fa-copy"></i></div></div><div class="v-list__tile__title">复制链接</div></a></div></div><input type="text" tabindex="-1" aria-hidden="true" value="" class="fake-hide"></div></div></div></nav><main class="v-content" style="padding-top:56px;padding-right:0px;padding-bottom:0px;padding-left:0px;"><div class="v-content__wrap"><div class="container blog-container grid-list-xl align-center"><div class="layout row wrap"><div class="flex mb-3 xs12"><article class="v-card elevation-16 post-card" style="height:undefined;"><div class="v-card__title"><div class="flex xs12"><h2 class="display-1 mb-3">18 Observable Operators - switchMap, mergeMap, concatMap</h2><div class="post-meta"><time datetime="2018-04-29T12:16:10.000Z" class="secondary--text post-time">2018年04月29日</time></div></div></div><div class="v-card__text pt-0 pb-0"><div class="flex xs12"><div class="content custom"><h1 id="_30-天精通-rxjs-18-observable-operators-switchmap-mergemap-concatmap"><a href="#_30-天精通-rxjs-18-observable-operators-switchmap-mergemap-concatmap" aria-hidden="true" class="header-anchor">#</a> 30 天精通 RxJS(18): Observable Operators - switchMap, mergeMap, concatMap</h1><p>今天我们要讲三个非常重要的 operators，这三个 operators 在很多的 RxJS 相关的 library 的使用示例上都会看到。很多初学者在使用这些 library 时，看到这三个 operators 很可能就放弃了，但其实如果有把这个系列的文章完整看过的话，现在应该就能很好接受跟理解。
</p><h2 id="operators"><a href="#operators" aria-hidden="true" class="header-anchor">#</a> Operators</h2><h3 id="concatmap"><a href="#concatmap" aria-hidden="true" class="header-anchor">#</a> concatMap</h3><p>concatMap 其实就是 map 加上 concatAll 的简化写法，我们直接来看一个示例</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e =&gt; Rx.Observable.interval(1000).take(3))
                .concatAll();

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>上面这个示例就可以简化成</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .concatMap(
                    e =&gt; Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>前后两个行为是一致的，记得 concatMap 也会先处理前一个送出的 observable 在处理下一个 observable，画成 Marble Diagram 如下</p><pre class="language-text"><code>source : -----------c--c------------------...
        concatMap(c =&gt; Rx.Observable.interval(100).take(3))
example: -------------0-1-2-0-1-2---------...

</code></pre><p>这样的行为也很常被用在发送 request 如下</p><pre class="language-text"><code>function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(
                    e =&gt; Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p><a href="https://jsbin.com/nuhita/5/edit?js,console,output" target="_blank" rel="noopener noreferrer">JSBin</a> | <a href="https://jsfiddle.net/t2zxtuh0/2/" target="_blank" rel="noopener noreferrer">JSFiddle</a></p><p>这里我们每点击一下画面就会送出一个 HTTP request，如果我们快速的连续点击，大家可以在开发者工具的 network 看到每个 request 是等到前一个 request 完成才会送出下一个 request，如下图</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1483454601/30days/concatMap_request.png" alt></p><blockquote><p>这里建议把网速模拟调到最慢</p></blockquote><p><img src="http://res.cloudinary.com/dohtkyi84/image/upload/v1483503231/30days/throttle_network.png" alt></p><p>从 network 的图形可以看得出来，第二个 request 的发送时间是接在第一个 request 之后的，我们可以确保每一个 request 会等前一个 request 完成才做处理。</p><p>concatMap 还有第二个参数是一个 selector callback，这个 callback 会传入四个参数，分别是</p><ol><li>外部 observable 送出的元素</li><li>内部 observable 送出的元素</li><li>外部 observable 送出元素的 index</li><li>内部 observable 送出元素的 index</li></ol><p>回传值我们想要的值，示例如下</p><pre class="language-text"><code>function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(
                e =&gt; Rx.Observable.from(getPostData()), 
                (e, res, eIndex, resIndex) =&gt; res.title);

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p><a href="https://jsbin.com/nuhita/7/edit?js,console,output" target="_blank" rel="noopener noreferrer">JSBin</a> | <a href="https://jsfiddle.net/t2zxtuh0/3/" target="_blank" rel="noopener noreferrer">JSFiddle</a></p><p>这个示例的外部 observable 送出的元素就是 click event 实例，内部 observable 送出的元素就是 response 实例，这里我们回传 response 实例的 title 属性，这样一来我们就可以直接收到 title，这个方法很适合用在 response 要选取的值跟前一个事件或顺位(index)相关时。</p><h3 id="switchmap"><a href="#switchmap" aria-hidden="true" class="header-anchor">#</a> switchMap</h3><p>switchMap 其实就是 map 加上 switch 简化的写法，如下</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e =&gt; Rx.Observable.interval(1000).take(3))
                .switch();

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>上面的代码可以简化成</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .switchMap(
                    e =&gt; Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>画成 Marble Diagram 表示如下</p><pre class="language-text"><code>source : -----------c--c-----------------...
        concatMap(c =&gt; Rx.Observable.interval(100).take(3))
example: -------------0--0-1-2-----------...

</code></pre><p>只要注意一个重点 switchMap 会在下一个 observable 被送出后直接退订前一个未处理完的 observable，这个部份的细节请看上一篇文章 switch 的部分。</p><p>另外我们也可以把 switchMap 用在发送 HTTP request</p><pre class="language-text"><code>function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.switchMap(
                    e =&gt; Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p><a href="https://jsbin.com/nuhita/1/edit?js,console,output" target="_blank" rel="noopener noreferrer">JSBin</a> | <a href="https://jsfiddle.net/t2zxtuh0/4/" target="_blank" rel="noopener noreferrer">JSFiddle</a></p><p>如果我们快速的连续点击五下，可以在开发者工具的 network 看到每个 request 会在点击时发送，如下图</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1483456745/30days/switchMap_request.png" alt></p><blockquote><p>灰色是浏览器原生地停顿行为，实际上灰色的一开始就是 fetch 执行送出 request，只是卡在浏览器等待发送。</p></blockquote><p>从上图可以看到，虽然我们发送了多个 request 但最后真正印出来的 log 只会有一个，代表前面发送的 request 已经不会造成任何的 side-effect 了，这个很适合用在只看最后一次 request 的情境，比如说 自动完成(auto complete)，我们只需要显示使用者最后一次打在画面上的文字，来做建议选项而不用每一次的。</p><p>switchMap 跟 concatMap 一样有第二个参数 selector callback 可用来回传我们要的值，这部分的行为跟 concatMap 是一样的，这里就不再赘述。</p><h3 id="mergemap"><a href="#mergemap" aria-hidden="true" class="header-anchor">#</a> mergeMap</h3><p>mergeMap 其实就是 map 加上 mergeAll 简化的写法，如下</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .map(e =&gt; Rx.Observable.interval(1000).take(3))
                .mergeAll();

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>上面的代码可以简化成</p><pre class="language-text"><code>var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source
                .mergeMap(
                    e =&gt; Rx.Observable.interval(100).take(3)
                );

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>画成 Marble Diagram 表示</p><pre class="language-text"><code>source : -----------c-c------------------...
        concatMap(c =&gt; Rx.Observable.interval(100).take(3))
example: -------------0-(10)-(21)-2----------...

</code></pre><p>记得 mergeMap 可以并行处理多个 observable，以这个例子来说当我们快速点按两下，元素发送的时间点是有机会重叠的，这个部份的细节大家可以看上一篇文章 merge 的部分。</p><p>另外我们也可以把 switchMap 用在发送 HTTP request</p><pre class="language-text"><code>function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.mergeMap(
                    e =&gt; Rx.Observable.from(getPostData()));

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p><a href="https://jsbin.com/nuhita/3/edit?js,console,output" target="_blank" rel="noopener noreferrer">JSBin</a> | <a href="https://jsfiddle.net/t2zxtuh0/5/" target="_blank" rel="noopener noreferrer">JSFiddle</a></p><p>如果我们快速的连续点击五下，大家可以在开发者工具的 network 看到每个 request 会在点击时发送并且会 log 出五个实例，如下图</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1483457934/30days/mergeMap_request.png" alt></p><p>mergeMap 也能传入第二个参数 selector callback，这个 selector callback 跟 concatMap 第二个参数也是完全一样的，但 mergeMap 的重点是我们可以传入第三个参数，来限制并行处理的数量</p><pre class="language-text"><code>function getPostData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.mergeMap(
                e =&gt; Rx.Observable.from(getPostData()), 
                (e, res, eIndex, resIndex) =&gt; res.title, 3);

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p><a href="https://jsbin.com/nuhita/4/edit?js,console,output" target="_blank" rel="noopener noreferrer">JSBin</a> | <a href="https://jsfiddle.net/t2zxtuh0/5/" target="_blank" rel="noopener noreferrer">JSFiddle</a></p><p>这里我们传入 3 就能限制，HTTP request 最多只能同时送出 3 个，并且要等其中一个完成在处理下一个，如下图</p><p><img src="https://res.cloudinary.com/dohtkyi84/image/upload/v1483458530/30days/mergeMap_3_request.png" alt></p><p>大家可以注意看上面这张图，我连续点按了五下，但第四个 request 是在第一个完成后才送出的，这个很适合用在特殊的需求下，可以限制同时发送的 request 数量。</p><blockquote><p>RxJS 5 还保留了 mergeMap 的别名叫 flatMap，虽然官方文件上没有，但这两个方法是完全一样的。请参考<a href="https://github.com/ReactiveX/RxJS/issues/333" target="_blank" rel="noopener noreferrer">这里</a></p></blockquote><h3 id="switchmap-mergemap-concatmap"><a href="#switchmap-mergemap-concatmap" aria-hidden="true" class="header-anchor">#</a> switchMap, mergeMap, concatMap</h3><p>这三个 operators 还有一个共同的特性，那就是这三个 operators 可以把第一个参数所回传的 promise 实例直接转成 observable，这样我们就不用再用 <code>Rx.Observable.from</code> 转一次，如下</p><pre class="language-text"><code>function getPersonData() {
    return fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then(res =&gt; res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');

var example = source.concatMap(e =&gt; getPersonData());
                                    //直接回传 promise 实例

example.subscribe({
    next: (value) =&gt; { console.log(value); },
    error: (err) =&gt; { console.log('Error: ' + err); },
    complete: () =&gt; { console.log('complete'); }
});

</code></pre><p>至於在使用上要如何选择这三个 operators？ 其实都还是看使用情境而定，这里笔者简单列一下大部分的使用情境</p><ul><li>concatMap 用在可以确定<strong>内部的 observable 结束时间比外部 observable 发送时间来快的情境</strong>，并且不希望有任何并行处理行为，适合少数要一次一次完成到底的的 UI 动画或特别的 HTTP request 行为。</li><li>switchMap 用在只要最后一次行为的结果，适合绝大多数的使用情境。</li><li>mergeMap 用在并行处理多个 observable，适合需要并行处理的行为，像是多个 I/O 的并行处理。</li></ul><blockquote><p>建议初学者不确定选哪一个时，使用 switchMap</p></blockquote><blockquote><p>在使用 concatAll 或 concatMap 时，请注意内部的 observable 一定要能够的结束，且外部的 observable 发送元素的速度不能比内部的 observable 结束时间快太多，不然会有 memory issues</p></blockquote><h2 id="今日小结"><a href="#今日小结" aria-hidden="true" class="header-anchor">#</a> 今日小结</h2><p>今天的文章内容主要讲了三个 operators，如果有看完上一篇文章的读者应该不难吸收，主要还是使用情境上需要思考以及注意一些细节。</p><p>不知道今天读者有没有收获呢？ 如果有任何问题，欢迎留言给我，谢谢</p></div></div></div><div class="v-card__actions"><div class="flex xs12"><a href="/BLOG/tags/RXJS"><span tabindex="0" class="v-chip capitalize chip-tag v-chip--label v-chip--small"><span class="v-chip__content">RXJS</span></span></a></div></div></article></div><div class="flex text-xs-left xs6"><a href="/BLOG/posts/rxjs19.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text"><i class="fa mr-1 fa-chevron-left"></i>Prev</div><div class="title mt-1 primary--text hidden-xs-only">19 实际示例 - 简单 Auto Complete 实例</div></div></a></div><div class="flex text-xs-right xs6"><a href="/BLOG/posts/rxjs17.html" class="post-nav v-btn v-btn--flat v-btn--router"><div class="v-btn__content"><div class="grey--text">Next
          <i class="fa ml-1 fa-chevron-right"></i></div><div class="title mt-1 primary--text hidden-xs-only">17 Observable Operators - switch, mergeAll, concatAll</div></div></a></div><div class="flex mt-3 xs12"><div class="v-card" style="height:undefined;"><div class="v-card__title"><span class="headline">Comment</span></div></div></div></div></div><footer class="v-footer blog-footer darken-1 mt-3 theme--dark" style="height:auto;"><div class="primary--text text--lighten-4 text-xs-center py-3 v-card v-card--flat v-card--tile primary" style="height:undefined;"><div class="v-card__text pb-0">博客内容遵循 <a rel="license noopener noreferrer" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" target="_blank">知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议</a></div><div class="v-card__text pt-0 mt-1"><span>訾绍飞 © 2015 - 2018</span><span><!---->
        Power by
        <a href="https://vuepress.vuejs.org" target="_blank" rel="noopener noreferrer">VuePress</a> Theme
        <a href="https://github.com/ShaofeiZi/BLOG" target="_blank" rel="noopener noreferrer">indigo</a></span></div></div></footer></div></main><button type="button" class="v-btn v-btn--bottom v-btn--floating v-btn--fixed v-btn--right accent" style="display:none;"><div class="v-btn__content"><i class="fa fa-lg fa-chevron-up"></i></div></button></div></div></div>
    <script src="/BLOG/assets/js/16.cd99d056.js" defer></script><script src="/BLOG/assets/js/app.1a725be8.js" defer></script>
  </body>
</html>
